package core.servlets;

import static core.util.StoredData.REFRESH_COUNTER;
import static core.util.StoredData.TIME_OUT_MILLIS;
import static core.util.StoredData.TIME_OUT_SECONDS;
import static core.util.StoredData.putResponseCounter;
import static core.util.StoredData.storedSessionData;
import static core.util.Utilities.COOKIE_NAME;
import static core.util.Utilities.RPC_SOCKET_TIMEOUT;
import static core.util.Utilities.generateUniqueSessionId;
import static core.util.Utilities.redirectPage;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import core.db.DBWrapper;
import core.network.DBSyncThread;
import core.network.GetRequest;
import core.network.PutRequest;
import core.network.RPCListenThread;
import core.network.marshal.MarshalUtils;
import core.util.IpPortPair;
import core.util.Locations;
import core.util.MyCookieValue;
import core.util.PageText;
import core.util.ResponseCounter;
import core.util.StoredData;
import core.util.UserSessionData;
import core.util.Utilities;

public class HomeServlet extends HttpServlet {
	/**
     * 
     */
	private static final long serialVersionUID = -8449992157639174322L;

	private static ConcurrentMap<String, IpPortPair> groupMembership = new ConcurrentHashMap<String, IpPortPair>();

	private static AtomicInteger curRPCId = new AtomicInteger(Integer.MIN_VALUE);

	public static ConcurrentMap<String, IpPortPair> getGroupMembership() {
		return groupMembership;
	}

	public void setGroupMembership(HashMap<String, IpPortPair> dbTable) {
		for (String s : dbTable.keySet()) {
			groupMembership.put(s, dbTable.get(s));
		}
	}

	public static DBWrapper dbWrapper;

	private String serverId;

	private final long serverStartTime = System.currentTimeMillis();

	private DatagramSocket outSocket;

	private InetAddress servletAddr;

	private int servletPort;

	public int getServletPort() {
		return servletPort;
	}

	public DatagramSocket getOutSocket() {
		return outSocket;
	}

	private static AtomicInteger requestCounter = new AtomicInteger(0);

	private static AtomicInteger uniqueCounter = new AtomicInteger(0);

	public HomeServlet() {
		serverId = UUID.randomUUID().toString();
		java.net.InetAddress host = null;
		try {
			host = java.net.InetAddress.getLocalHost();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} finally {
			servletAddr = host;
		}

		try {
			outSocket = new DatagramSocket();
			outSocket.setSoTimeout(RPC_SOCKET_TIMEOUT);
		} catch (SocketException e1) {
			e1.printStackTrace();
		}
		servletPort = outSocket.getLocalPort();

		new RPCListenThread(this).start();

		try {
			dbWrapper = new DBWrapper(this);
		} catch (IOException e) {
			e.printStackTrace();
		}

		new DBSyncThread(dbWrapper).start();
	}

	@Override
	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		System.out.println("Now Sync to DB...");
		dbWrapper.updateGroupMembership();

		// garbage collection happens every 1000 requests
		if (requestCounter.compareAndSet(REFRESH_COUNTER, 0)) {
			// remove stale session data
			StoredData.removeStaleSessionData();
		}

		// get cookies
		Cookie[] cookies = request.getCookies();

		// get session id from request
		String sessionId;
		try {
			sessionId = generateUniqueSessionId(request,
					servletAddr.getHostAddress(), serverStartTime,
					uniqueCounter);
		} catch (NoSuchAlgorithmException e1) {
			e1.printStackTrace();
			throw new ServletException(e1);
		}

		MyCookieValue cookieValue = null;
		// get our cookie
		Cookie cookie = null;
		if (cookies != null) {
			for (Cookie c : cookies) {
				cookie = c;
				if (COOKIE_NAME.equalsIgnoreCase(cookie.getName())) {
					cookieValue = MyCookieValue.buildFromString(cookie
							.getValue());
					break;
				}
			}
		}

		// no cookie found in request, this is a new session
		if (cookieValue == null || !StoredData.isCookieFresh(cookieValue)) {
			System.out.println("No cookie... New session");
			cookieValue = createNewCookieValue(sessionId, serverId);
			cookie = new Cookie(COOKIE_NAME, cookieValue.toString());
			cookie.setMaxAge(TIME_OUT_SECONDS);
			UserSessionData data = new UserSessionData(0, "Hello, User!",
					cookieValue);
			storedSessionData.put(cookieValue.getSessionId(), data);
		}
		else if (!StoredData.isCookieValid(cookieValue)) {
			// if the cookie is not new and not valid
			System.out.println("the cookie is not new and not valid");
	//		redirectPage(response, "error.html");
		}

		processRequest(cookie, response, cookieValue);
	}

	/**
	 * Begin processing request
	 * 
	 * @param cookieValue
	 * @throws IOException
	 */
	private void processRequest(Cookie cookie, HttpServletResponse response,
			MyCookieValue cookieValue) throws IOException {

		UserSessionData data;
		Locations locations = cookieValue.getLocation();
		String sessionId = cookieValue.getSessionId();

		if (locations.contains(serverId)) {
			data = storedSessionData.get(sessionId);
		} else {
			System.out
					.println("This session is not in this server, RPC GET is being sent...");
			data = RPCGetSessionData(sessionId, locations);
			// update session data with the values obtained from GET
		}

		if (data == null) {
			// we need to do something if we can't load the session.
			// for now
			throw new IOException("Couldn't load session data");
		}

		// increase version number on cookie value
		cookieValue.increaseVersion();
		cookieValue.refreshExpireTime();

		// add cookie to session data
		data.setCookieValue(cookieValue);
		// set last active time
		data.setLastActiveTime(System.currentTimeMillis());
		// increase display count
		data.increaseDisplayNumber();

		// we want to send PUT requests to other servers here
		int callId = curRPCId.incrementAndGet();
		sendPutRequests(cookie, callId, data, cookieValue, response);
	}

	private UserSessionData RPCGetSessionData(String sessionId,
			Locations locations) throws SocketException {
		int callId = curRPCId.incrementAndGet();
		byte[] bytesToSend;
		int timesSent = 0;
		
		try {
			bytesToSend = MarshalUtils.objectToByteArray(new GetRequest(callId,
					sessionId));
		} catch (IOException e) {
			System.out.println("no rpc get data to send.");
			return null;
		}
		for (String location : locations.getValues()) {
			IpPortPair ipPortPair = groupMembership.get(location);

			if (ipPortPair != null) {
				System.out.println("Sending out RPC get to "
						+ ipPortPair.getIp() + " : " + ipPortPair.getPort());
				DatagramPacket packetToSend = new DatagramPacket(bytesToSend,
						bytesToSend.length, ipPortPair.getIp(),
						ipPortPair.getPort());
				try {
					outSocket.setSoTimeout(RPC_SOCKET_TIMEOUT);
					outSocket.send(packetToSend);
					timesSent++;
				} catch (IOException e) {
					System.out.println("Sending failed.");
				}
			} else {
				System.out.println("requested location not in table");
			}
		}
		if (timesSent == 0) {
			System.out.println("The session has no location at all.");
			return null;
		}
		UserSessionData data = null;
		try {
			do {
				data = StoredData.getResponse.get(callId);
			} while (data == null);
			System.out.println("Received data from GET response");
		} catch (Exception e) {
			System.out.println("Exception in getting GET response.");
			return null;
		}
		return data;
	}

	private void sendPutRequests(Cookie cookie, int callId,
			UserSessionData data, MyCookieValue cookieValue,
			HttpServletResponse response) {
		Locations newLocations = new Locations();
		String sessionId = cookieValue.getSessionId();

		// Do it yourself first
		StoredData.storedSessionData.put(sessionId, data);
		newLocations.add(serverId);

		putResponseCounter.put(callId, new ResponseCounter());
		PutRequest req = new PutRequest(callId, data);
		if (groupMembership.keySet().size() - 1 < Utilities.PUT_SEND) {
			response.setContentType("text/html");
			PrintWriter out = null;
			try {
				out = response.getWriter();
			} catch (IOException e) {
				e.printStackTrace();
			}
			out.println(PageText.getHomeHTML(serverId,
					servletAddr.getHostAddress(), servletPort,
					data.getDisplayNumber(), data.getDisplayText()));
			// update cookie and attach to response
			cookieValue.setLocations(newLocations);
			cookie.setValue(cookieValue.toString());
			response.addCookie(cookie);
		} else {
			Set<String> targetServers = new HashSet<String>();
			while (targetServers.size() < Utilities.PUT_SEND) {
				String sId = getRandomServerId(groupMembership.keySet());
				if (!targetServers.contains(sId) && !sId.equals(serverId)) {
					targetServers.add(sId);
				}
			}
			for (String s : targetServers) {
				sendRPCPutRequest(req, groupMembership.get(s));
			}
			try {
				processPutResponse(cookie, callId, cookieValue, response,
						newLocations);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * Send PutRequest to target IpPortPair
	 * 
	 * @param req
	 * 
	 * @param ipPortPair
	 */
	private void sendRPCPutRequest(PutRequest req, IpPortPair ipPortPair) {
		byte[] bytesToSend = null;
		DatagramPacket packetToSend = null;

		try {
			bytesToSend = MarshalUtils.objectToByteArray(req);
		} catch (IOException e) {
			System.out.println("put req to byte array error.");
		}
		packetToSend = new DatagramPacket(bytesToSend, bytesToSend.length,
				ipPortPair.getIp(), ipPortPair.getPort());
		try {
			outSocket.send(packetToSend);
			System.out.println("Send RPC PUT to " + ipPortPair.getIp() + " : "
					+ ipPortPair.getPort() + " successful!");
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("Send RPC PUT to " + ipPortPair.getIp() + " : "
					+ ipPortPair.getPort() + " failed!");
		}
	}

	private static String getRandomServerId(Set<String> serverIdSet) {
		int number = new SecureRandom().nextInt(serverIdSet.size());
		int i = 0;
		for (String obj : serverIdSet) {
			if (i == number)
				return obj;
			i = i + 1;
		}
		return null;
	}

	private void processPutResponse(Cookie cookie, int callId,
			MyCookieValue cookieValue, HttpServletResponse response,
			Locations newLocations) throws IOException {
		ResponseCounter rc = putResponseCounter.get(callId);
		while (rc.getCount() < Utilities.PUT_RECV
				&& rc.getFirstCallTime() + ResponseCounter.TIME_OUT_DURATION > System
						.currentTimeMillis()) {
			// do nothing
		}
		if (rc.getFirstCallTime() + ResponseCounter.TIME_OUT_DURATION <= System
				.currentTimeMillis()) {
			// TODO: this is a time out redirect to timed out page?
			System.out.println("Waiting PUT response TIMEOUT!!!");
		} else if (rc.getCount() >= Utilities.PUT_RECV) {
			System.out.println("PUT response collected!!!");
			Locations locs = putResponseCounter.get(callId).getLocations();
			for (String s : locs.getValues()) {
				newLocations.add(s);
			}
			putResponseCounter.remove(callId);
			cookieValue.setLocations(newLocations);
			// update cookie and attach to response
			cookie.setValue(cookieValue.toString());
			response.addCookie(cookie);

			UserSessionData data = storedSessionData.get(cookieValue
					.getSessionId());
			response.setContentType("text/html");
			PrintWriter out = response.getWriter();
			out.println(PageText.getHomeHTML(serverId,
					servletAddr.getHostAddress(), servletPort,
					data.getDisplayNumber(), data.getDisplayText()));

		} else {
			// this should never happen...
			System.out.println("this should never happen...");
			redirectPage(response, "error.html");
		}
	}

	private MyCookieValue createNewCookieValue(String sessionId, String location) {
		return new MyCookieValue(sessionId, 0, System.currentTimeMillis()
				+ TIME_OUT_MILLIS, location);
	}

	public InetAddress getIP() {
		return servletAddr;
	}

	public String getServerId() {
		return serverId;
	}

	/*
	 * public void setServerId(String serverId) { HomeServlet.serverId =
	 * serverId; }
	 */
}
